﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Runtime.InteropServices;

namespace BlinkyDevice
{
    /// <summary>
    /// Custom USB device exception
    /// </summary>
    public class HIDDeviceException : ApplicationException
    {
        public HIDDeviceException(string strMessage) : base(strMessage) { }

        public static HIDDeviceException GenerateWithWinError(string strMessage)
        {
            return new HIDDeviceException(string.Format("Msg:{0} WinEr:{1:X8}", strMessage, Marshal.GetLastWin32Error()));
        }
    }

    /// <summary>
    /// Abstract USB device - device controller derived from this
    /// </summary>
    public abstract class HID : UsbImports, IDisposable
    {
        /// <summary>Filestream used for reading and writing</summary>
        private FileStream file;
        /// <summary>Input report length given by device</summary>
        private int iReportLength;
        /// <summary>Output report length given by device</summary>
        private int oReportLength;
        /// <summary>Handle to the device</summary>
        private IntPtr handle;

        /// <summary>
        /// Dispose
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Disposer called by dispose and finalize
        /// </summary>
        /// <param name="disposing">True if disposing</param>
        protected virtual void Dispose(bool disposing)
        {
            // if we are disposing, need to close the managed resources
            if (disposing)
            {
                if (file != null)
                {
                    file.Close();
                    file = null;
                }
            }
            // Dispose and finalize, get rid of unmanaged resources
            if (handle != IntPtr.Zero)	
            {
                CloseHandle(handle);
            }
        }

        /// <summary>
        /// Initialize the device
        /// </summary>
        /// <param name="path">Path of the device</param>
        private void Initialize(string path)
        {
            // Create the file from the device path
            handle = CreateFile(path, GENERIC_READ | GENERIC_WRITE, 0, IntPtr.Zero, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, IntPtr.Zero);
            // If file creation was successful
            if (handle != InvalidHandleValue)	
            {
                IntPtr lpData;
                // Read the device data into an internal buffer
                if (HidD_GetPreparsedData(handle, out lpData))	
                {
                    try
                    {
                        HidCaps caps;
                        // Gets device capabilities from device firmware
                        HidP_GetCaps(lpData, out caps);	
                        // Get the input report length
                        iReportLength = caps.InputReportByteLength;
                        // Get the output report length
                        oReportLength = caps.OutputReportByteLength;
                        // Filestream to read/write to device
                        file = new FileStream(handle, FileAccess.Read | FileAccess.Write, true, inputReportLength, true);
                        // Begin asynchronous reading
                        BeginAsyncRead();
                    }
                    finally
                    {
                        // MUST free internal buffer reserved by GetPreparsedData
                        HidD_FreePreparsedData(ref lpData);
                    }
                }
                else	// GetPreparsedData failed
                {
                    throw HIDDeviceException.GenerateWithWinError("GetPreparsedData failed");
                }
            }
            else	// File creation failed
            {
                handle = IntPtr.Zero;
                throw HIDDeviceException.GenerateWithWinError("Failed to create device file");
            }
        }

        /// <summary>
        /// Asynchronous reading completes when data is read or device disconnected - uses callback
        /// </summary>
        private void BeginAsyncRead()
        {
            byte[] arrInputReport = new byte[inputReportLength];
            // put the buff we used to receive the stuff as the async state then we can get at it when the read completes
            file.BeginRead(arrInputReport, 0, inputReportLength, new AsyncCallback(ReadCompleted), arrInputReport);
        }

        protected void ReadCompleted(IAsyncResult iResult)
        {
            // Get the read buffer
            byte[] arrBuff = (byte[])iResult.AsyncState;
            try
            {
                // This will throw any exceptions which occured while reading
                file.EndRead(iResult);
                try
                {
                    // Create the input report for the device
                    InputReport inRep = CreateInputReport();
                    // Set the data portion - processes data into more easily readable format
                    inRep.SetData(arrBuff);
                    // Pass the input report to the handler
                    HandleDataReceived(inRep);
                }
                finally
                {
                    // Read the next report
                    BeginAsyncRead();
                }
            }
            // Occurs if the device is removed
            catch (IOException ex)
            {
                HandleDeviceRemoved();
                if (OnDeviceRemoved != null)
                {
                    OnDeviceRemoved(this, new EventArgs());
                }
                Dispose();
            }
        }

        /// <summary>
        /// Write an ouput report to the device
        /// </summary>
        /// <param name="outRep">Output report to be written</param>
        protected void Write(OutputReport outRep)
        {
            try
            {
                file.Write(outRep.buffer, 0, outRep.bufferLength);
            }
            catch (IOException ex)
            {
                // Occurs if the device is removed
                throw new HIDDeviceException("Device was removed");
            }
        }

        /// <summary>
        /// Virtual handler for action to be taken when data is received
        /// </summary>
        /// <param name="inRep">The input report that was received</param>
        protected virtual void HandleDataReceived(InputReport inRep)
        {

        }

        /// <summary>
        /// Virtual handler for actions to occur when device removed
        /// </summary>
        protected virtual void HandleDeviceRemoved()
        {

        }

        /// <summary>
        /// Returns the device path - used in 'FindDevice'
        /// </summary>
        /// <param name="infoSet">Handle of the InfoSet</param>
        /// <param name="devInterface">DeviceInterfaceData structure</param>
        /// <returns></returns>
        private static string GetDevicePath(IntPtr infoSet, ref DeviceInterfaceData devInterface)
        {
            uint requiredSize = 0;
            // Get the device interface details
            if (!SetupDiGetDeviceInterfaceDetail(infoSet, ref devInterface, IntPtr.Zero, 0, ref requiredSize, IntPtr.Zero))
            {
                DeviceInterfaceDetailData detail = new DeviceInterfaceDetailData();
                detail.Size = 5;
                if (SetupDiGetDeviceInterfaceDetail(infoSet, ref devInterface, ref detail, requiredSize, ref requiredSize, IntPtr.Zero))
                {
                    return detail.DevicePath;
                }
            }
            return null;
        }

        /// <summary>
        /// Finds the device given a VID and PID
        /// </summary>
        /// <param name="vid">Vendor ID of the device</param>
        /// <param name="pid">Product ID of the device</param>
        /// <param name="type">Type of device class to create</param>
        /// <returns>A new device of the given type or null</returns>
        public static HID FindDevice(int vid, int pid, Type type)
        {
            // Build the path search string
            string search = string.Format("vid_{0:x4}&pid_{1:x4}", vid, pid);
            Guid gHid;
            // Retrieve the GUID from Windows that represents the USB interface
            HidD_GetHidGuid(out gHid);
            // List of all HIDs currently connected to the computer
            IntPtr infoSet = SetupDiGetClassDevs(ref gHid, null, IntPtr.Zero, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
            try
            {
                // Build a DeviceInterfaceData block
                DeviceInterfaceData devInterface = new DeviceInterfaceData();
                devInterface.Size = Marshal.SizeOf(devInterface);
                // Iterate through the InfoSet assignedin the call to SetupDiGetClassDevs to retrieve details for every device connected
                int index = 0;
                // Gets device info for device at 'index' in the block
                while (SetupDiEnumDeviceInterfaces(infoSet, 0, ref gHid, (uint)index, ref devInterface))
                {
                    // Retrieve device path
                    string strDevicePath = GetDevicePath(infoSet, ref devInterface);
                    // If the VID/PID string matches, device is found
                    if (strDevicePath.IndexOf(search) >= 0)
                    {
                        // Create an instance of this device
                        HID newDevice = (HID)Activator.CreateInstance(type);
                        // Initialize it with the path
                        newDevice.Initialize(strDevicePath);
                        // Return the device
                        return newDevice;
                    }
                    // This wasn't our device - check the next one
                    index++;
                }
            }
            finally
            {
                // MUST free InfoSet memory reserved by SetupDiGetClassDevs
                SetupDiDestroyDeviceInfoList(infoSet);
            }
            // No currently connected device matchs
            return null;
        }

        /// <summary>
        /// Event handler for when device is removed
        /// </summary>
        public event EventHandler OnDeviceRemoved;

        /// <summary>
        /// Accessor for the output report length
        /// </summary>
        public int outputReportLength
        {
            get
            {
                return oReportLength;
            }
        }

        /// <summary>
        /// Accessor for the input report length
        /// </summary>
        public int inputReportLength
        {
            get
            {
                return iReportLength;
            }
        }
        
        /// <summary>
        /// Virtual method to create an input report for this device
        /// </summary>
        /// <returns>Created input report</returns>
        public virtual InputReport CreateInputReport()
        {
            return null;
        }
    }
}
